#define _GNU_SOURCE
#include <X11/Xft/Xft.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xinerama.h>
#include <ctype.h>
#include <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define SchemeNorm 0
#define SchemeSel 1
#define SchemeDir 2
#define SchemeLast 3

static const char *colors[SchemeLast][2] = {
    [SchemeNorm] = {"#ffffff", "#222222"},
    [SchemeSel] = {"#000000", "#ffff00"},
    [SchemeDir] = {"#00ff00", "#222222"},
};

static const char *font_names[] = {"WenQuanYi Micro Hei:size=14:antialias=true",
                                   "Noto Sans CJK SC:size=14:antialias=true",
                                   "Microsoft YaHei:size=14:antialias=true",
                                   "SimHei:size=14:antialias=true",
                                   "DejaVu Sans Mono:size=14:antialias=true",
                                   "Monaco:size=14:antialias=true",
                                   "Consolas:size=14:antialias=true",
                                   "monospace:size=14:antialias=true",
                                   NULL};

// 文件扩展名-打开程序映射表
typedef struct {
  const char *ext; // 扩展名（小写）
  const char *cmd; // 打开命令（%s 替换为文件路径）
} FileAssociation;

static const FileAssociation file_assocs[] = {
    // 文本文件
    {".txt", "geany %s"},
    {".md", "geany %s"},
    {".conf", "geany %s"},
    {".json", "geany %s"},
    {".xml", "geany %s"},
    {".yml", "code %s"},
    // 图片文件
    {".jpg", "feh %s"},
    {".jpeg", "feh %s"},
    {".png", "feh %s"},
    {".gif", "feh %s"},
    {".bmp", "feh %s"},
    {".svg", "feh %s"},
    // 音频文件（强制用ffplay）
    {".wav", "ffplay -autoexit %s"},
    {".mp3", "ffplay -autoexit %s"},
    {".flac", "ffplay -autoexit %s"},
    {".ogg", "ffplay -autoexit %s"},
    {".m4a", "ffplay -autoexit %s"},
    {".aac", "ffplay -autoexit %s"},
    // 视频文件
    {".mp4", "mpv %s"},
    {".mkv", "mpv %s"},
    {".avi", "mpv %s"},
    {".mov", "mpv %s"},
    {".flv", "mpv %s"},
    // 文档文件
    {".pdf", "zathura %s"},
    {".docx", "libreoffice %s"},
    {".xlsx", "libreoffice %s"},
    {".pptx", "libreoffice %s"},
    // 脚本/可执行文件
    {".sh", "x-terminal-emulator -e bash %s"},
    {".py", "python3 %s"},
    {".c", "code %s"},
    {".cpp", "code %s"},
    {NULL, NULL} // 结束标记
};

static unsigned int win_w = 1280, win_h = 720;
static int curr_offset = 0;
static int sel_idx = 0;
static int items_per_page = 0;

typedef struct {
  char name[NAME_MAX];
  char path[PATH_MAX];
  int is_dir;
} Entry;
static Entry *entries = NULL;
static int entry_count = 0;

static char current_dir[PATH_MAX];
static char history[64][PATH_MAX];
static int history_len = 0;

static int compare_entries(const void *a, const void *b) {
  Entry *ea = (Entry *)a;
  Entry *eb = (Entry *)b;
  if (ea->is_dir && !eb->is_dir)
    return -1;
  if (!ea->is_dir && eb->is_dir)
    return 1;
  return strcmp(ea->name, eb->name);
}

// 获取文件扩展名（小写）
static const char *get_file_ext(const char *filename) {
  const char *dot = strrchr(filename, '.');
  if (!dot || dot == filename)
    return "";
  char *ext = strdup(dot);
  for (char *p = ext; *p; p++)
    *p = tolower(*p);
  return ext;
}

// 根据文件路径获取打开命令
static const char *get_open_command(const char *filepath) {
  const char *filename = strrchr(filepath, '/');
  filename = filename ? filename + 1 : filepath;
  const char *ext = get_file_ext(filename);
  static char cmd[PATH_MAX + 64];

  // 查找匹配的关联程序
  for (int i = 0; file_assocs[i].ext; i++) {
    if (strcmp(ext, file_assocs[i].ext) == 0) {
      snprintf(cmd, sizeof(cmd), file_assocs[i].cmd, filepath);
      free((void *)ext);
      return cmd;
    }
  }

  // 无匹配时使用系统默认程序
  snprintf(cmd, sizeof(cmd), "xdg-open %s", filepath);
  free((void *)ext);
  return cmd;
}

// 打开文件
static void open_file(const char *filepath) {
  const char *cmd = get_open_command(filepath);
  // 后台执行命令，避免阻塞程序
  char bg_cmd[PATH_MAX + 128];
  snprintf(bg_cmd, sizeof(bg_cmd), "%s &", cmd);
  system(bg_cmd);
}

static void load_directory(const char *dir) {
  if (entries)
    free(entries);
  entries = NULL;
  entry_count = 0;

  DIR *dp = opendir(dir);
  if (!dp)
    return;

  struct dirent *dentry;
  while ((dentry = readdir(dp)) && entry_count < 4096) {
    if (dentry->d_name[0] == '.' && strcmp(dentry->d_name, ".") != 0 &&
        strcmp(dentry->d_name, "..") != 0)
      continue;

    char fullpath[PATH_MAX];
    snprintf(fullpath, PATH_MAX, "%s/%s", dir, dentry->d_name);

    struct stat st;
    int is_dir = 0;
    if (stat(fullpath, &st) == 0)
      is_dir = S_ISDIR(st.st_mode);

    entries = realloc(entries, (entry_count + 1) * sizeof(Entry));
    strncpy(entries[entry_count].name, dentry->d_name, NAME_MAX);
    strncpy(entries[entry_count].path, fullpath, PATH_MAX);
    entries[entry_count].is_dir = is_dir;
    entry_count++;
  }
  closedir(dp);
  qsort(entries, entry_count, sizeof(Entry), compare_entries);
  sel_idx = 0;
  curr_offset = 0;
}

static void enter_directory(const char *dir) {
  if (history_len == 0 || strcmp(current_dir, history[history_len - 1]) != 0) {
    if (history_len < 64)
      strncpy(history[history_len++], current_dir, PATH_MAX);
  }

  realpath(dir, current_dir);
  load_directory(current_dir);
}

static void go_back() {
  if (history_len > 0) {
    strncpy(current_dir, history[--history_len], PATH_MAX);
    load_directory(current_dir);
  }
}

static void copy_to_clipboard(const char *text) {
  char cmd[PATH_MAX + 64];
  if (access("/usr/bin/xclip", X_OK) == 0) {
    snprintf(cmd, sizeof(cmd), "echo -n '%s' | xclip -selection clipboard",
             text);
    system(cmd);
  } else if (access("/usr/bin/wl-copy", X_OK) == 0) {
    snprintf(cmd, sizeof(cmd), "echo -n '%s' | wl-copy", text);
    system(cmd);
  }
}

typedef struct {
  Display *dpy;
  unsigned int h;
  XftFont *xfont;
} Fnt;

enum { ColFg, ColBg };
typedef XftColor Clr;

typedef struct {
  unsigned int w, h;
  Display *dpy;
  int screen;
  Window root;
  Drawable drawable;
  GC gc;
  Clr *scheme;
  Fnt *font;
} Drw;

Drw *drw_create(Display *dpy, int screen, Window root) {
  Drw *drw = calloc(1, sizeof(Drw));
  if (!drw)
    return NULL;
  drw->dpy = dpy;
  drw->screen = screen;
  drw->root = root;
  drw->w = win_w;
  drw->h = win_h;
  drw->drawable =
      XCreatePixmap(dpy, root, drw->w, drw->h, DefaultDepth(dpy, screen));
  drw->gc = XCreateGC(dpy, root, 0, NULL);
  return drw;
}

Fnt *font_create(Drw *drw) {
  if (!drw)
    return NULL;
  Fnt *f = calloc(1, sizeof(Fnt));
  if (!f)
    return NULL;
  f->dpy = drw->dpy;
  XftFont *xfont = NULL;
  for (int i = 0; font_names[i]; i++) {
    xfont = XftFontOpenName(drw->dpy, drw->screen, font_names[i]);
    if (xfont)
      break;
  }
  if (!xfont) {
    fprintf(stderr, "Warning: Using default font\n");
    xfont = XftFontOpenName(drw->dpy, drw->screen, "monospace:size=12");
    if (!xfont) {
      fprintf(stderr, "Error: No font\n");
      free(f);
      return NULL;
    }
  }
  f->xfont = xfont;
  f->h = xfont->ascent + xfont->descent;
  return f;
}

void drw_clr_create(Drw *drw, Clr *dest, const char *name) {
  if (!drw || !dest)
    return;
  if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
                         DefaultColormap(drw->dpy, drw->screen), name, dest))
    fprintf(stderr, "Cannot allocate color: %s\n", name);
}

void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h,
              int filled) {
  if (!drw)
    return;
  XSetForeground(drw->dpy, drw->gc, drw->scheme[ColBg].pixel);
  if (filled)
    XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
  else
    XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1);
}

void drw_text(Drw *drw, int x, int y, const char *text) {
  if (!drw || !text)
    return;
  int ty = y + (drw->font->h / 2) + (drw->font->xfont->ascent / 2);

  XGlyphInfo ext;
  XftTextExtentsUtf8(drw->dpy, drw->font->xfont, (XftChar8 *)text, strlen(text),
                     &ext);
  if ((unsigned int)ext.xOff > drw->w - 40) {
    char truncated[1024];
    strncpy(truncated, text, 50);
    truncated[50] = '\0';
    strcat(truncated, "...");
    XftDraw *d = XftDrawCreate(drw->dpy, drw->drawable,
                               DefaultVisual(drw->dpy, drw->screen),
                               DefaultColormap(drw->dpy, drw->screen));
    XftDrawStringUtf8(d, &drw->scheme[ColFg], drw->font->xfont, x, ty,
                      (XftChar8 *)truncated, strlen(truncated));
    XftDrawDestroy(d);
  } else {
    XftDraw *d = XftDrawCreate(drw->dpy, drw->drawable,
                               DefaultVisual(drw->dpy, drw->screen),
                               DefaultColormap(drw->dpy, drw->screen));
    XftDrawStringUtf8(d, &drw->scheme[ColFg], drw->font->xfont, x, ty,
                      (XftChar8 *)text, strlen(text));
    XftDrawDestroy(d);
  }
}

void drw_refresh(Drw *drw, Window win) {
  if (!drw)
    return;
  XCopyArea(drw->dpy, drw->drawable, win, drw->gc, 0, 0, drw->w, drw->h, 0, 0);
  XSync(drw->dpy, False);
}

void draw_list(Drw *drw, Clr *norm, Clr *sel, Clr *dir) {
  if (!drw || !norm || !sel || !dir)
    return;

  int status_bar_h = drw->font->h * 2;
  int list_h = drw->h - status_bar_h;
  items_per_page = list_h / drw->font->h;
  if (items_per_page <= 0)
    items_per_page = 1;

  drw->scheme = norm;
  drw_rect(drw, 0, 0, drw->w, drw->h, 1);

  int list_y = 0;
  for (int i = curr_offset; i < entry_count && i < curr_offset + items_per_page;
       i++) {
    if (i == sel_idx)
      drw->scheme = sel;
    else if (entries[i].is_dir)
      drw->scheme = dir;
    else
      drw->scheme = norm;

    drw_rect(drw, 0, list_y, drw->w, drw->font->h, 1);
    char disp_name[NAME_MAX + 2];
    strcpy(disp_name, entries[i].name);
    if (entries[i].is_dir)
      strcat(disp_name, "/");
    drw_text(drw, 10, list_y, disp_name);
    list_y += drw->font->h;
  }

  drw->scheme = sel;
  drw_rect(drw, 0, drw->h - status_bar_h, drw->w, status_bar_h, 1);
  drw->scheme = sel;
  drw_text(drw, 10, drw->h - status_bar_h, current_dir);
  char help[512] = "Enter=Open/Enter dir | Backspace=Go back | "
                   "Click=Open/Enter dir | q=Quit | Path copied to clipboard";
  drw_text(drw, 10, drw->h - status_bar_h + drw->font->h, help);
}

void handle_mouse(Drw *drw, Clr *norm, Clr *sel, Clr *dir, XButtonEvent *ev) {
  if (!drw || !ev)
    return;

  if (ev->button == 4) {
    sel_idx = (sel_idx == 0) ? entry_count - 1 : sel_idx - 1;
    curr_offset = sel_idx - (sel_idx % items_per_page);
  } else if (ev->button == 5) {
    sel_idx = (sel_idx == entry_count - 1) ? 0 : sel_idx + 1;
    curr_offset = sel_idx - (sel_idx % items_per_page);
  } else if (ev->type == ButtonPress) {
    int click_idx = curr_offset + (ev->y / drw->font->h);
    if (click_idx >= 0 && click_idx < entry_count) {
      Entry *e = &entries[click_idx];
      if (e->is_dir) {
        enter_directory(e->path);
      } else {
        printf("%s\n", e->path);
        copy_to_clipboard(e->path);
        open_file(e->path); // 打开文件
        exit(0);
      }
    }
  }

  draw_list(drw, norm, sel, dir);
  drw_refresh(drw, ev->window);
}

void event_loop(Display *dpy, Window win, Drw *drw, Clr *norm, Clr *sel,
                Clr *dir) {
  XEvent ev;
  while (1) {
    XNextEvent(dpy, &ev);
    switch (ev.type) {
    case Expose:
      if (ev.xexpose.count == 0) {
        draw_list(drw, norm, sel, dir);
        drw_refresh(drw, win);
      }
      break;
    case ButtonPress:
      handle_mouse(drw, norm, sel, dir, &ev.xbutton);
      break;
    case KeyPress: {
      KeySym ks = XLookupKeysym(&ev.xkey, 0);
      if (ks == XK_Escape || ks == XK_q) {
        exit(0);
      } else if (ks == XK_Return) {
        if (sel_idx >= 0 && sel_idx < entry_count) {
          Entry *e = &entries[sel_idx];
          if (e->is_dir) {
            enter_directory(e->path);
          } else {
            printf("%s\n", e->path);
            copy_to_clipboard(e->path);
            open_file(e->path); // 打开文件
            exit(0);
          }
        }
      } else if (ks == XK_BackSpace) {
        go_back();
      } else if (ks == XK_Up) {
        sel_idx = (sel_idx == 0) ? entry_count - 1 : sel_idx - 1;
        curr_offset = sel_idx - (sel_idx % items_per_page);
      } else if (ks == XK_Down) {
        sel_idx = (sel_idx == entry_count - 1) ? 0 : sel_idx + 1;
        curr_offset = sel_idx - (sel_idx % items_per_page);
      }
      draw_list(drw, norm, sel, dir);
      drw_refresh(drw, win);
      break;
    }
    case DestroyNotify:
      exit(0);
    }
  }
}

Window create_window(Display *dpy) {
  if (!dpy)
    return 0;
  int screen = DefaultScreen(dpy);
  Window root = RootWindow(dpy, screen);
  XSetWindowAttributes swa;

  int x = 0, y = 0;
  XineramaScreenInfo *info;
  int n;
  if ((info = XineramaQueryScreens(dpy, &n))) {
    XWindowAttributes wa;
    XGetWindowAttributes(dpy, root, &wa);
    x = (wa.width - win_w) / 2;
    y = (wa.height - win_h) / 2;
    XFree(info);
  }

  swa.override_redirect = True;
  swa.background_pixel = 0;
  swa.event_mask =
      ExposureMask | ButtonPressMask | DestroyNotify | KeyPressMask;
  Window win =
      XCreateWindow(dpy, root, x, y, win_w, win_h, 0, DefaultDepth(dpy, screen),
                    InputOutput, DefaultVisual(dpy, screen),
                    CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
  XMapRaised(dpy, win);
  XSetInputFocus(dpy, win, RevertToParent, CurrentTime);
  return win;
}

void usage() {
  fprintf(stderr, "Usage: guifm [OPTIONS]\n");
  fprintf(stderr, "Simple GUI file manager (start at $HOME)\n");
  fprintf(stderr, "Options:\n");
  fprintf(stderr, "  -h        Show help\n");
  fprintf(stderr, "  -w <width>  Window width (default: 1280)\n");
  fprintf(stderr, "  -H <height> Window height (default: 720)\n");
  exit(0);
}

int main(int argc, char **argv) {
  for (int i = 1; i < argc; i++) {
    if (!strcmp(argv[i], "-h"))
      usage();
    else if (!strcmp(argv[i], "-w") && i + 1 < argc)
      win_w = atoi(argv[++i]);
    else if (!strcmp(argv[i], "-H") && i + 1 < argc)
      win_h = atoi(argv[++i]);
    else
      usage();
  }

  char *home_dir = getenv("HOME");
  if (home_dir && strlen(home_dir) > 0) {
    strncpy(current_dir, home_dir, PATH_MAX);
  } else {
    strcpy(current_dir, "/");
  }
  load_directory(current_dir);

  Display *dpy = XOpenDisplay(NULL);
  if (!dpy) {
    fprintf(stderr, "Error: Cannot open X display\n");
    return 1;
  }

  int screen = DefaultScreen(dpy);
  Window root = RootWindow(dpy, screen);
  Drw *drw = drw_create(dpy, screen, root);
  if (!drw) {
    fprintf(stderr, "Error: Cannot create drawer\n");
    XCloseDisplay(dpy);
    return 1;
  }
  drw->font = font_create(drw);
  if (!drw->font) {
    fprintf(stderr, "Error: Cannot load font\n");
    XCloseDisplay(dpy);
    free(drw);
    return 1;
  }

  Clr norm[2], sel[2], dir[2];
  drw_clr_create(drw, &norm[ColFg], colors[SchemeNorm][ColFg]);
  drw_clr_create(drw, &norm[ColBg], colors[SchemeNorm][ColBg]);
  drw_clr_create(drw, &sel[ColFg], colors[SchemeSel][ColFg]);
  drw_clr_create(drw, &sel[ColBg], colors[SchemeSel][ColBg]);
  drw_clr_create(drw, &dir[ColFg], colors[SchemeDir][ColFg]);
  drw_clr_create(drw, &dir[ColBg], colors[SchemeDir][ColBg]);

  Window win = create_window(dpy);
  if (!win) {
    fprintf(stderr, "Error: Cannot create window\n");
    XCloseDisplay(dpy);
    free(drw->font);
    free(drw);
    return 1;
  }

  draw_list(drw, norm, sel, dir);
  drw_refresh(drw, win);
  event_loop(dpy, win, drw, norm, sel, dir);

  free(entries);
  XCloseDisplay(dpy);
  free(drw->font);
  free(drw);
  return 0;
}
